package org.activequant.container.context;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.activequant.container.ITradeSystemContext;
import org.activequant.container.ITradeSystemContextThread;
import org.activequant.container.ITradeSystemHandle;
import org.activequant.tradesystem.annotation.Inject;
import org.activequant.tradesystem.annotation.Start;
import org.activequant.tradesystem.annotation.Stop;
import org.apache.log4j.Logger;

/**
 * An abstract class. Implements the ITradeSystemContext interface. Holds the following associated variables:
 * <ul>
 * <li>tradeSystems(List&lt;ITradeSystemHandle&gt;)</li>
 * <li>isRunning(boolean)</li>
 * <li>factory(IObjectFactory)</li>
 * </ul>
 * <br>
 *       <b>History:</b><br>
 *       - [16.01.2008] Created (Erik Nijkamp)<br>
 * 
 * @author Erik Nijkamp
 */
public abstract class TradeSystemContextBase implements ITradeSystemContext {

	protected final Logger log = Logger.getLogger(getClass());
	/**
	 * private final List&lt;ITradeSystemHandle&gt; tradeSystems = new ArrayList&lt;ITradeSystemHandle&gt;();
	 */
	private final List<ITradeSystemHandle> tradeSystems = new ArrayList<ITradeSystemHandle>();
	/**
	 * protected boolean isRunning = false;
	 */
	protected boolean isRunning = false;
	/**
	 * an abstract method. to be implemented
	 * @param <T>
	 * @param clazz
	 * @param name
	 * @return
	 */
	protected abstract <T> T lookup(Class<T> clazz, String name);

	// object factory that delegates to the subclass' lookup() method.
	// note that it exports itself
	/**
	 * object factory that delegates to the subclass' lookup() method.
	 * note that it exports itself<br/>
	 * private final IObjectFactory factory = new IObjectFactory() {...}<br/>
	 * An IObjectFactory implementation whose lookup(Class&lt;T&gt; clazz, String name) method invokes the abstract lookup(Class&lt;T&gt; clazz, String name) method of this 
	 * TradeSystemContextBase class.
	 */
	private final IObjectFactory factory = new IObjectFactory() {

		@SuppressWarnings("unchecked")
		public <T> T lookup(Class<T> clazz, String name) {
			if (name == null && clazz.equals(IObjectFactory.class)) {
				return (T) this;
			}
			return TradeSystemContextBase.this.lookup(clazz, name);
		}
	};
	/**
	 * sets every Field declared in the given system(Object) (and its subclasses) who is annotated by the Inject annotation with the 
	 * object(Object) looked up under the name(an attribute of the Inject annotation) and Class of the given system(Object).<br/>
	 * Then it does the same for all Inject annotated setter methods.
	 * @param system
	 * @throws Exception
	 */
	// ############# reflection #############
	private void inject(Object system) throws Exception {
		// field injection
		for (Field field : getFieldsByAnnotation(system.getClass(), Inject.class)) {
			// get object type
			Class<?> type = field.getType();
			// get object
			Inject annotation = field.getAnnotation(Inject.class);
			String name = getAnnotationName(annotation);
			Object object = factory.lookup(type, name);
			// set object
			if (object != null) {
				if (!field.isAccessible())
					field.setAccessible(true);
				field.set(system, object);
			} else if (!annotation.optional()) {
				throw new IllegalArgumentException("Non-optional object '" + type.getSimpleName() + "' is null (not found).");
			}
		}

		// method injection
		for (Method setter : getMethodsByAnnotation(system.getClass(), Inject.class)) {
			// check signature
			if (setter.getReturnType() != void.class)
				continue;
			if (setter.getParameterTypes().length != 1)
				continue;
			Class<?> service = setter.getParameterTypes()[0];
			// get service object
			Inject annotation = setter.getAnnotation(Inject.class);
			String name = getAnnotationName(annotation);
			Object object = factory.lookup(service, name);
			// set object
			if (object != null) {
				setter.invoke(system, new Object[] { object });
			} else if (!annotation.optional()) {
				throw new IllegalArgumentException("Non-optional object for class '" + service.getSimpleName() + "' with name '" + name + "' is null (not found).");
			}
		}
	}
	/**
	 * returns the value of the name(String) attribute of the given annotation(Inject) (or null if the value is "")
	 * @param annotation
	 * @return
	 */
	private String getAnnotationName(Inject annotation) {
		return annotation.name().equals("") ? null : annotation.name();
	}
	/**
	 * returns a Method[] array holding the methods from the given clazz(Class&lt;?&gt;) who are annotated with the given annotation(Class&lt;? extends Annotation&gt;)
	 * @param clazz
	 * @param annotation
	 * @return
	 */
	private Method[] getMethodsByAnnotation(Class<?> clazz, Class<? extends Annotation> annotation) {
		List<Method> methods = new ArrayList<Method>();
		for (Method method : clazz.getMethods()) {
			if (method.isAnnotationPresent(annotation)) {
				methods.add(method);
			}
		}
		return methods.toArray(new Method[] {});
	}
	/**
	 * returns a Field[] array holding the fields declared in the given clazz(Class&lt;?&gt;) (and its subclasses)
	 * who are annotated with the given annotation(Class&lt;? extends Annotation&gt;)
	 * @param clazz
	 * @param annotation
	 * @return
	 */
	private Field[] getFieldsByAnnotation(Class<?> clazz, Class<? extends Annotation> annotation) {
		List<Field> fields = new ArrayList<Field>();
		for (Field field : getFields(clazz)) {
			if (field.isAnnotationPresent(annotation)) {
				fields.add(field);
			}
		}
		return fields.toArray(new Field[] {});
	}
	/**
	 * returns a Field[] array holding all the fields declared in the given clazz(Class&lt;?&gt;) and its subclasses
	 * @param clazz
	 * @return
	 */
	private Field[] getFields(Class<?> clazz) {
		List<Field> fields = new ArrayList<Field>();
		for (Class<?> parent : getSuperClasses(clazz)) {
			Field[] declaredFields = parent.getDeclaredFields();
			fields.addAll(Arrays.asList(declaredFields));
		}
		return fields.toArray(new Field[] {});
	}
	/**
	 * returns a Class&lt;?&gt;[] array holding the given clazz(Class&lt;?&gt;) and its subclasses
	 * @param clazz
	 * @return
	 */
	private Class<?>[] getSuperClasses(Class<?> clazz) {
		List<Class<?>> classes = new ArrayList<Class<?>>();
		Class<?> main = clazz;
		while (main != null) {
			classes.add(main);
			main = main.getSuperclass();
		}
		return classes.toArray(new Class<?>[] {});
	}
	/**
	 * invokes every one of the given methods(Method[]) on the given object(Object) without any arguments
	 * @param methods
	 * @param object
	 * @throws Exception
	 */
	private void invoke(Method[] methods, Object object) throws Exception {
		for (Method method : methods) {
			method.invoke(object, new Object[] {});
		}
	}
	
	// ############# context #############
	/**
	 * Registers and returns an ITradeSystemHandle for the given tradeSystem(Object)<br/>
	 * <code>1.</code> Sets every Field declared in the given tradeSystem(Object) (and its subclasses) who is annotated by the Inject annotation with the 
	 * object(Object) looked up under the name(an attribute of the Inject annotation) and Class of the given system(Object).<br/>
	 * Then it does the same for all Inject annotated setter methods in the given tradeSystem(Object).<br/>
	 * <code>2.</code> Creates and returns an ITradeSystemHandle for the given tradeSystem(Object). It also adds it to the associated tradeSystems(List&lt;ITradeSystemHandle&gt;). 
	 * This ITradeSystemHandle's <code>start()</code> method invokes all Start annotated methods in the given tradeSystem(Object), whereas its
	 * <code>stop()</code> method invokes all Stop annotated methods in the given tradeSystem(Object).
	 */
	public ITradeSystemHandle add(final Object tradeSystem) throws Exception {
		// setup
		inject(tradeSystem);

		// handle
		ITradeSystemHandle handle = new ITradeSystemHandle() {
			public void remove() {
				tradeSystems.remove(this);
			}

			public void start() throws Exception {
				Method[] start = getMethodsByAnnotation(tradeSystem.getClass(), Start.class);
				invoke(start, tradeSystem);
			}

			public void stop() throws Exception {
				Method[] stop = getMethodsByAnnotation(tradeSystem.getClass(), Stop.class);
				invoke(stop, tradeSystem);
			}
		};

		// register
		tradeSystems.add(handle);
		return handle;
	}
	/**
	 * invokes <code>tradeSystem.remove()</code> on every tradeSystem in the associated tradeSystems(List&lt;ITradeSystemHandle&gt;)
	 */
	public void clear() throws Exception {
		for (ITradeSystemHandle tradeSystem : tradeSystems) {
			tradeSystem.remove();
		}
	}
	/**
	 * if the associated isRunning(boolean) is true, it will stop all associated tradeSystems(List&lt;ITradeSystemHandle&gt;) before starting them again and setting 
	 * isRunning(boolean) to true again.<br/>
	 * If there is an ITradeSystemContextThread specifies/mapped in the associated factory(IObjectFactory), it starts running that as well.
	 */
	public void start() throws Exception {
		// stop
		if (isRunning)
			stop();
		log.info("Starting " + this.getClass().getSimpleName());

		// start
		for (ITradeSystemHandle tradeSystem : tradeSystems) {
			tradeSystem.start();
		}

		// running
		isRunning = true;

		// run scan backtest replay
//		final Runnable runnableScanner = factory.lookup(ITradeSystemContextThread.class, "scanMain");
//		if (runnableScanner != null) {
//			log.info("running context thread: " + runnableScanner.getClass().getSimpleName());
//			runnableScanner.run();
//		}
		
		// run
		final Runnable runnable = factory.lookup(ITradeSystemContextThread.class, null);
		if (runnable != null) {
			log.info("running context thread: " + runnable.getClass().getSimpleName());
			runnable.run();
		}
		

	}
	/**
	 * invokes <code>tradeSystem.stop()</code> on every tradeSystem in the associated tradeSystems(List&lt;ITradeSystemHandle&gt;).<br/>
	 * Then it sets the associated isRunning(boolean) to false
	 */
	public void stop() throws Exception {
		for (ITradeSystemHandle tradeSystem : tradeSystems) {
			tradeSystem.stop();
		}
		isRunning = false;
	}
	/**
	 * returns the associated isRunning(boolean)
	 */
	public boolean isRunning() {
		return isRunning;
	}
	/**
	 * returns a ITradeSystemHandle[] array holding the associated tradeSystems(List&lt;ITradeSystemHandle&gt;)
	 * @return
	 */
	public ITradeSystemHandle[] getTradeSystemHandles() {
		return tradeSystems.toArray(new ITradeSystemHandle[] {});
	}

}
